﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace MashGen.Engine.Expressions
{
    public class ExpressionComparer : IComparer<AbstractExpression>
    {
        private const string UNSUPPORTED_TYPE_MESSAGE = "Unsupported type.";

        static ExpressionComparer() { }

        private static ExpressionComparer instance = new ExpressionComparer();

        public static ExpressionComparer Instance
        {
            get { return instance; }
        }

        private IList<Type> compareOrder;

        private ExpressionComparer()
        {
            compareOrder = new List<Type> {
                typeof(Number),
                typeof(Variable),
                typeof(Sum),
                typeof(Product),
                typeof(Power),
            };
        }

        public int Compare(AbstractExpression left, AbstractExpression right)
        {
            if (ReferenceEquals(left, right))
            {
                return 0;
            }
            if (ReferenceEquals(left, null))
            {
                return -1;
            }
            if (ReferenceEquals(right, null))
            {
                return +1;
            }
            return CompareNotNull(left, right, +1);
        }

        private int CompareNotNull(AbstractExpression left, AbstractExpression right, int numberOrder)
        {
            if (ReferenceEquals(left, right))
            {
                return 0;
            }

            Type leftType = left.GetType();
            Type rightType = right.GetType();

            if (leftType == rightType)
            {
                if (left.Equals(right))
                {
                    return 0;
                }
                if (leftType == typeof(Number))
                {
                    return ((Number)left).Value < ((Number)right).Value ? -numberOrder : numberOrder;
                }
                if (leftType == typeof(Variable))
                {
                    return ((Variable)left).Name.CompareTo(((Variable)right).Name);
                }
                if (leftType == typeof(Negative))
                {
                    return CompareNotNull(((Negative)left).Expression, ((Negative)right).Expression, +1);
                }
                if (leftType == typeof(Sum))
                {
                    return CompareMany<Sum>(left, right, s => s.NormalizedSummands);
                }
                if (leftType == typeof(Product))
                {
                    return CompareMany<Product>(left, right, p => p.NormalizedFactors);
                }

                if (leftType == typeof(Quotient))
                {
                    return CompareTwo<Quotient>(left, right, q => q.Denominator, +1, q => q.Numerator, +1);
                }
                if (leftType == typeof(Power))
                {
                    return CompareTwo<Power>(left, right, p => p.Exponent, -1, q => q.Radix, +1);
                }

                throw new ArgumentException(UNSUPPORTED_TYPE_MESSAGE);
            }

            if (leftType == typeof(Negative))
            {
                return CompareSpecial<Negative>(left, right, n => n.Expression);
            }
            if (rightType == typeof(Negative))
            {
                return -CompareSpecial<Negative>(right, left, n => n.Expression);
            }
            
            if (leftType == typeof(Quotient))
            {
                return CompareSpecial<Quotient>(left, right, q => q.Numerator);
            }
            if (rightType == typeof(Quotient))
            {
                return -CompareSpecial<Quotient>(right, left, q => q.Numerator);
            }

            int leftOrder = compareOrder.IndexOf(leftType);
            if (leftOrder < 0)
            {
                throw new ArgumentException(UNSUPPORTED_TYPE_MESSAGE, "left");
            }
            int rightOrder = compareOrder.IndexOf(rightType);
            if (rightOrder < 0)
            {
                throw new ArgumentException(UNSUPPORTED_TYPE_MESSAGE, "right");
            }
            return leftOrder < rightOrder ? +1 : -1;
        }

        private int CompareTwo<T>(AbstractExpression left, AbstractExpression right, 
                Func<T, AbstractExpression> firstAccessor,  int firstNumberOrder,
                Func<T, AbstractExpression> secondAccessor, int secondNumberOrder)
            where T : AbstractExpression
        {
            int result = CompareNotNull(firstAccessor((T)left), firstAccessor((T)right), firstNumberOrder);
            if (result != 0)
            {
                return result;
            }
            return CompareNotNull(secondAccessor((T)left), secondAccessor((T)right), secondNumberOrder);
        }

        private int CompareMany<T>(AbstractExpression left, AbstractExpression right, Func<T, IList<AbstractExpression>> accessor)
            where T : AbstractExpression
        {
            var leftElements = accessor((T)left);
            var rightElements = accessor((T)right);
            int count = Math.Min(leftElements.Count, rightElements.Count);

            for (int index = 0; index < count; index++)
            {
                int result = CompareNotNull(leftElements[index], rightElements[index], +1);
                if (result != 0)
                {
                    return result;
                }
            }

            if (leftElements.Count < count)
            {
                return -1;
            }
            if (rightElements.Count < count)
            {
                return +1;
            }
            return 0;
        }

        private int CompareSpecial<T>(AbstractExpression special, AbstractExpression other,
                Func<T, AbstractExpression> accessor)
            where T : AbstractExpression
        {
            var inner = accessor((T)special);
            int result = CompareNotNull(inner, other, +1);
            if (result != 0)
            {
                return result;
            }
            return 1;
        }
    }
}
